namespace ScrumSprintMonitor.Modules.WorkItemMonitor.ViewModels
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Globalization;
	using System.Linq;
	using System.Threading;
	using System.Windows;
	using System.Windows.Media;
	using System.Windows.Threading;

	using Infrastructure;
	using Infrastructure.Configuration;

	using UI.Wpf.Converters;
	using UI.Wpf.ViewModels;

	#endregion

	[DebuggerDisplay("ViewModel: {DisplayName}")]
	public class TeamMemberViewModel : ViewModelBase,
	                                   ITeamMemberViewModel
	{
		static TeamMemberViewModel()
		{
			var dependsOn = Register<TeamMemberViewModel>();

			dependsOn.Call(me => me.AttachToBurndownDataPoints()).OnChanged(me => me.Model.BurndownDataPoints);
			dependsOn.Call(me => me.OnPropertyChanged("BurndownChartVisibility")).OnChanged(me => me.CurrentSprint);
			dependsOn.Call(me => me.OnPropertyChanged("BurndownChartStartDate")).OnChanged(me => me.CurrentSprint.Start);
			dependsOn.Call(me => me.OnPropertyChanged("BurndownChartEndDate")).OnChanged(me => me.CurrentSprint.End);
			dependsOn.Call(me => me.OnPropertyChanged("IsBuildingBurndownDataPoints")).OnChanged(me => me.CurrentSprint.Burndown.IsBuildingDataPoints);
			dependsOn.Call(me => me.OnPropertyChanged("MaximumBurndownChartHours")).OnChanged(
				me => me.TeamMembersListViewModel.MaximumTeamMemberBurndownChartHours);
			dependsOn.Call(me => me.RecalculateExcludedDates()).OnChanged(me => me.BurndownChartStartDate).OnChanged(me => me.BurndownChartEndDate);
			dependsOn.Call(me => me.RecalculateBurndownChartTrendLine()).OnChanged(me => me.IsBuildingBurndownDataPoints);
			dependsOn.Call(me => me.RecalculateBurndownDependents()).OnChanged(me => me.CurrentSprintBurndownDataPoints);

			dependsOn.Call(me => me.OnPropertyChanged("AssignedWorkItems")).OnChanged(me => me.Model.AssignedWorkItems);
			dependsOn.Call(me => me.OnPropertyChanged("Unassigned")).OnChanged(me => me.Model.Information.Unassigned);
			dependsOn.Call(me => me.OnPropertyChanged("DisplayName")).OnChanged(me => me.Model.DisplayName);
			dependsOn.Call(me => me.OnPropertyChanged("AvatarImageSource")).OnChanged(me => me.Model.Information.Avatar);

			dependsOn.Call(me => me.OnPropertyChanged("RemainingPotentialSprintHours")).OnChanged(me => me.Model.RemainingWorkload.PotentialSprintHours);
			dependsOn.Call(me => me.OnPropertyChanged("RemainingSprintHours")).OnChanged(me => me.Model.RemainingWorkload.SprintHours);
			dependsOn.Call(me => me.OnRemainingWorkPropertyChanged("RemainingSprintWork")).OnChanged(me => me.Model.RemainingWorkload.SprintWork);
			dependsOn.Call(me => me.OnRemainingWorkPropertyChanged("RemainingUnassignedSprintWork")).OnChanged(
				me => me.Model.RemainingWorkload.UnassignedSprintWork);
			dependsOn.Call(me => me.OnRemainingWorkPropertyChanged("RemainingWorkItemCount")).OnChanged(me => me.Model.RemainingWorkload.WorkItemCount);
			dependsOn.Call(me => me.OnPropertyChanged("IncludesFreeBusyInformation")).OnChanged(me => me.Model.RemainingWorkload.IncludesFreeBusyInformation);

			dependsOn.Call(me => me.UpdateData()).OnChanged(me => me.AssignedWorkItems);
			dependsOn.Call(me => me.OnPropertyChanged("RemainingSprintWorkWithUnassigned")).OnChanged(me => me.RemainingSprintWork).OnChanged(
				me => me.RemainingUnassignedSprintWork);
			dependsOn.Call(me => me.OnPropertyChanged("IsActiveInSprint")).OnChanged(me => me.RemainingWorkItemCount).OnChanged(me => me.RemainingSprintWork);
			dependsOn.Call(me => me.OnPropertyChanged("BackgroundColor")).OnChanged(me => me.RemainingSprintHours).OnChanged(
				me => me.RemainingSprintWorkWithUnassigned);
		}

		public TeamMemberViewModel(ITeamMember model, ISprint currentSprint, IShellLayoutConfiguration shellLayoutConfiguration,
		                           ITeamMemberFreeBusyInfo freeBusyInfo, ITeamMembersListViewModel teamMembersListViewModel)
		{
			if (model == null)
			{
				throw new ArgumentNullException("model");
			}
			if (currentSprint == null)
			{
				throw new ArgumentNullException("currentSprint");
			}
			if (shellLayoutConfiguration == null)
			{
				throw new ArgumentNullException("shellLayoutConfiguration");
			}
			if (freeBusyInfo == null)
			{
				throw new ArgumentNullException("freeBusyInfo");
			}

			this.initialized = false;
			this.shellLayoutConfiguration = shellLayoutConfiguration;
			this.freeBusyInfo = freeBusyInfo;

			this.backlogItemsPieChartData = new ObservableCollection<PieChartValue<int>>();
			this.workloadPieChartData = new ObservableCollection<PieChartValue<double>>();
			for (int index = 0; index < 4; ++index)
			{
				this.backlogItemsPieChartData.Add(new PieChartValue<int>
				                                  	{
				                                  		Value = 0
				                                  	});
				this.workloadPieChartData.Add(new PieChartValue<double>
				                              	{
				                              		Value = 0.0
				                              	});
			}

			Model = model;
			CurrentSprint = currentSprint;
			TeamMembersListViewModel = teamMembersListViewModel;
		}

		#region Private Fields

		private readonly ObservableCollection<PieChartValue<int>> backlogItemsPieChartData;
		private readonly ObservableCollection<BurndownDataPoint> burndownDataPoints = new ObservableCollection<BurndownDataPoint>();
		private readonly ObservableCollection<BurndownDataPoint> burndownTrendLineDataPointList = new ObservableCollection<BurndownDataPoint>();
		private readonly ITeamMemberFreeBusyInfo freeBusyInfo;
		private readonly IShellLayoutConfiguration shellLayoutConfiguration;
		private readonly ObservableCollection<PieChartValue<double>> workloadPieChartData;
		private List<DateTime> excludedDatesList = new List<DateTime>();
		private ISprint currentSprint;
		private BindingList<BurndownDataPoint> currentSprintBurndownDataPoints;
		private double doneSprintWork;
		private int doneWorkItemCount;
		private bool initialized;
		private DateTime lastUpdate;
		private ITeamMember model;
		private bool notAllWorkItemsEstimated;
		private bool recentlyUpdated;
		private ITeamMembersListViewModel teamMembersListViewModel;

		#endregion

		#region ITeamMemberViewModel Members

		public ITeamMember Model
		{
			[DebuggerStepThrough]
			get { return this.model; }
			set
			{
				if (value != this.model)
				{
					this.model = value;

					OnPropertyChanged("Model");
				}
			}
		}

		public bool Unassigned
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return Model.Information.Unassigned;
			}
		}

		public string DisplayName
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return Model.DisplayName;
			}
		}

		public string Team
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return Model.Information.Team;
			}
		}

		public ImageSource AvatarImageSource
		{
			get
			{
				GuardDisposed();

				if (this.model.Information.Avatar == null)
				{
					return null;
				}

				return (ImageSource) this.model.Information.Avatar.GetRepresentation(typeof(ImageSource));
			}
		}

		public bool RecentlyUpdated
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.recentlyUpdated;
			}
			private set
			{
				GuardDisposed();

				if (value != this.recentlyUpdated)
				{
					this.recentlyUpdated = value;

					OnPropertyChanged("RecentlyUpdated");
				}
			}
		}

		public DateTime LastUpdate
		{
			set
			{
				GuardDisposed();

				if (value != this.lastUpdate)
				{
					this.lastUpdate = value;

					RefreshRecentlyUpdatedFlag();
				}
			}
		}

		public double RemainingUnassignedSprintWork
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingWorkload.UnassignedSprintWork;
			}
		}

		public ObservableCollection<PieChartValue<int>> BacklogItemsPieChartData
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.backlogItemsPieChartData;
			}
		}

		public ObservableCollection<PieChartValue<double>> WorkloadPieChartData
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.workloadPieChartData;
			}
		}

		public Visibility BurndownChartVisibility
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return (this.shellLayoutConfiguration.ShowBurndownChart && CurrentSprint != null ? Visibility.Visible : Visibility.Collapsed);
			}
		}

		public double MaximumBurndownChartHours
		{
			[DebuggerStepThrough]
			get { return TeamMembersListViewModel.MaximumTeamMemberBurndownChartHours; }
		}

		public DateTime BurndownChartStartDate
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return CurrentSprint.Start;
			}
		}

		public DateTime BurndownChartEndDate
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return CurrentSprint.End;
			}
		}

		public List<DateTime> ExcludedDates
		{
			[DebuggerStepThrough]
			get { return this.excludedDatesList; }
			private set
			{
				GuardDisposed();

				if (value == ExcludedDates)
				{
					return;
				}

				this.excludedDatesList = value;

				OnPropertyChanged("ExcludedDates");
			}
		}

		public ObservableCollection<BurndownDataPoint> BurndownDataPoints
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.burndownDataPoints;
			}
		}

		public ObservableCollection<BurndownDataPoint> BurndownTrendLineDataPoints
		{
			[DebuggerStepThrough]
			get { return this.burndownTrendLineDataPointList; }
		}

		public void RefreshRecentlyUpdatedFlag()
		{
			RecentlyUpdated = (SystemTime.Now() - this.lastUpdate).TotalMinutes < 5.0;
		}

		public IEnumerable<IWorkItemAdapter> AssignedWorkItems
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return Model.AssignedWorkItems;
			}
		}

		public bool NotAllWorkItemsEstimated
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.notAllWorkItemsEstimated;
			}
			private set
			{
				GuardDisposed();

				if (value != this.notAllWorkItemsEstimated)
				{
					this.notAllWorkItemsEstimated = value;

					OnPropertyChanged("NotAllWorkItemsEstimated");
				}
			}
		}

		public Color BackgroundColor
		{
			get
			{
				GuardDisposed();

				return
					(Color)
					new WorkTimeRatioBrushValueConverter().Convert(new object[] {RemainingSprintWorkWithUnassigned, RemainingSprintHours, NotAllWorkItemsEstimated},
					                                               typeof(Color), "Background", CultureInfo.InvariantCulture);
			}
		}

		public int DoneWorkItemCount
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.doneWorkItemCount;
			}
			private set
			{
				GuardDisposed();

				if (value != this.doneWorkItemCount)
				{
					this.doneWorkItemCount = value;

					OnPropertyChanged("DoneWorkItemCount");
					MarkUpdated();
				}
			}
		}

		public double DoneSprintWork
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.doneSprintWork;
			}
			private set
			{
				GuardDisposed();

				if (value != this.doneSprintWork)
				{
					this.doneSprintWork = value;

					OnPropertyChanged("DoneSprintWork");
					MarkUpdated();
				}
			}
		}

		public bool IsActiveInSprint
		{
			get { return RemainingSprintWork != 0.0 || RemainingWorkItemCount != 0; }
		}

		public int RemainingWorkItemCount
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingWorkload.WorkItemCount;
			}
		}

		public double RemainingSprintWork
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingWorkload.SprintWork;
			}
		}

		public double RemainingPotentialSprintHours
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingWorkload.PotentialSprintHours;
			}
		}

		public double RemainingSprintHours
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingWorkload.SprintHours;
			}
		}

		public bool IncludesFreeBusyInformation
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingWorkload.IncludesFreeBusyInformation;
			}
		}

		public double RemainingSprintWorkWithUnassigned
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return RemainingSprintWork + RemainingUnassignedSprintWork;
			}
		}

		#endregion

		#region Private Methods

		private ITeamMembersListViewModel TeamMembersListViewModel
		{
			[DebuggerStepThrough]
			get { return this.teamMembersListViewModel; }
			set
			{
				if (value == TeamMembersListViewModel)
				{
					return;
				}

				this.teamMembersListViewModel = value;

				OnPropertyChanged("TeamMembersListViewModel");
			}
		}

		private ITeamMemberRemainingWorkload RemainingWorkload
		{
			get { return (Model != null ? Model.RemainingWorkload : null); }
		}

		private ISprint CurrentSprint
		{
			[DebuggerStepThrough]
			get { return this.currentSprint; }
			set
			{
				GuardDisposed();

				if (value != CurrentSprint)
				{
					this.currentSprint = value;

					OnPropertyChanged("CurrentSprint");
				}
			}
		}

		public bool IsBuildingBurndownDataPoints
		{
			[DebuggerStepThrough]
			get { return (CurrentSprint != null && CurrentSprint.Burndown != null ? CurrentSprint.Burndown.IsBuildingDataPoints : false); }
		}

		private BindingList<BurndownDataPoint> CurrentSprintBurndownDataPoints
		{
			[DebuggerStepThrough]
			get { return this.currentSprintBurndownDataPoints; }
			set
			{
				if (value == CurrentSprintBurndownDataPoints)
				{
					return;
				}

				BurndownDataPoints.Clear();
				if (this.currentSprintBurndownDataPoints != null)
				{
					this.currentSprintBurndownDataPoints.ListChanged -= OnBurndownDataPointsListChanged;
				}

				this.currentSprintBurndownDataPoints = value;

				if (this.currentSprintBurndownDataPoints != null)
				{
					foreach (var burndownDataPoint in this.currentSprintBurndownDataPoints)
					{
						BurndownDataPoints.Add(burndownDataPoint);
					}
					this.currentSprintBurndownDataPoints.ListChanged += OnBurndownDataPointsListChanged;
				}

				OnPropertyChanged("CurrentSprintBurndownDataPoints");
			}
		}

		private void RecalculateBurndownDependents()
		{
			RecalculateBurndownChartTrendLine();
		}

		private void RecalculateBurndownChartTrendLine()
		{
			this.burndownTrendLineDataPointList.Clear();

			if (CurrentSprint == null || CurrentSprint.Burndown == null || IsBuildingBurndownDataPoints)
			{
				return;
			}

			BurndownChartHelper.CalculateBurndownTrendLineDataPointList(this.burndownTrendLineDataPointList, BurndownDataPoints,
																		CurrentSprint.Start, BurndownChartEndDate);
		}

		private void RecalculateExcludedDates()
		{
			var newExcludedDatesList = new List<DateTime>();

			for (var at = BurndownChartStartDate; at <= BurndownChartEndDate; at = at.AddDays(1.0))
			{
				var baseStationDate = at;
				if (!this.freeBusyInfo.IsWorkDayFor(Model.Information, baseStationDate))
				{
					newExcludedDatesList.Add(at);
					continue;
				}
			}

			ExcludedDates = newExcludedDatesList;
		}

		private void UpdateData()
		{
			if (AssignedWorkItems == null)
			{
				NotAllWorkItemsEstimated = false;
				DoneWorkItemCount = 0;
				DoneSprintWork = 0.0;
			}
			else
			{
				var remainingSprintBacklogItems =
					AssignedWorkItems.Where(
						x => x.Type == WorkItemType.SprintBacklogItem && (x.State == WorkItemState.InProgress || x.State == WorkItemState.NotDone));

				NotAllWorkItemsEstimated = remainingSprintBacklogItems.Any(x => double.IsNaN(x.RemainingWork));
				DoneWorkItemCount = AssignedWorkItems.Count(x => x.Type == WorkItemType.SprintBacklogItem && x.State == WorkItemState.Done);
				DoneSprintWork = AssignedWorkItems.Where(x => x.Type == WorkItemType.SprintBacklogItem).Sum(x => x.DoneWork);
			}

			PostAction(DispatcherPriority.DataBind, UpdatePieChartData);
		}

		private void UpdatePieChartData()
		{
			UpdateBacklogItemsPieChartData();
			UpdateWorkloadPieChartData();

			this.initialized = true;
		}

		private void UpdateBacklogItemsPieChartData()
		{
			int doneCount = 0;
			int activeCount = 0;
			int notDoneCount = 0;

			if (Model.AssignedWorkItems != null)
			{
				var assignedSprintBacklogItems = Model.AssignedWorkItems.Where(x => x.Type == WorkItemType.SprintBacklogItem);

				if (assignedSprintBacklogItems.Any())
				{
					Parallel.Invoke(() => doneCount = assignedSprintBacklogItems.Count(x => x.State == WorkItemState.Done),
					                () => activeCount = assignedSprintBacklogItems.Count(x => x.State == WorkItemState.InProgress),
					                () => notDoneCount = assignedSprintBacklogItems.Count(x => x.State == WorkItemState.NotDone));
				}
			}

			AssignPieChartElement(BacklogItemsPieChartData, 0, doneCount);
			AssignPieChartElement(BacklogItemsPieChartData, 1, 0);
			AssignPieChartElement(BacklogItemsPieChartData, 2, activeCount);
			AssignPieChartElement(BacklogItemsPieChartData, 3, notDoneCount);
		}

		private void UpdateWorkloadPieChartData()
		{
			double readyForTestWork = 0.0;
			double doneWorkInActiveItems = 0.0;
			double remainingWorkInActiveItems = 0.0;

			if (Model.AssignedWorkItems != null)
			{
				var assignedSprintBacklogItems = Model.AssignedWorkItems.Where(x => x.Type == WorkItemType.SprintBacklogItem);
				var activeSprintBacklogItems = assignedSprintBacklogItems.Where(x => x.State == WorkItemState.InProgress);

				if (activeSprintBacklogItems.Any())
				{
					Parallel.Invoke(() => readyForTestWork = assignedSprintBacklogItems.Where(x => x.State == WorkItemState.Done).Sum(x => x.DoneWork),
					                () => doneWorkInActiveItems = activeSprintBacklogItems.Sum(x => x.DoneWork),
					                () => remainingWorkInActiveItems = activeSprintBacklogItems.Sum(x => x.RemainingWork));
				}
			}

			AssignPieChartElement(WorkloadPieChartData, 0, readyForTestWork);
			AssignPieChartElement(WorkloadPieChartData, 1, doneWorkInActiveItems);
			AssignPieChartElement(WorkloadPieChartData, 2, remainingWorkInActiveItems);
			AssignPieChartElement(WorkloadPieChartData, 3, RemainingSprintWorkWithUnassigned - remainingWorkInActiveItems);
		}

		private void AssignPieChartElement<T>(IList<PieChartValue<T>> collection, int index, T value)
		{
			if (!Equals(value, collection[index].Value))
			{
				collection[index].Value = value;

				MarkUpdated();
			}
		}

		private void OnRemainingWorkPropertyChanged(string propertyName)
		{
			OnPropertyChanged(propertyName);

			MarkUpdated();
		}

		private void MarkUpdated()
		{
			if (this.initialized)
			{
				LastUpdate = SystemTime.Now();
			}
		}

		private void AttachToBurndownDataPoints()
		{
			CurrentSprintBurndownDataPoints = (Model != null ? Model.BurndownDataPoints : null);
		}

		private void OnBurndownDataPointsListChanged(object sender, ListChangedEventArgs e)
		{
			switch (e.ListChangedType)
			{
				case ListChangedType.ItemAdded:
					BurndownDataPoints.Add(CurrentSprintBurndownDataPoints[e.NewIndex]);
					RecalculateBurndownDependents();
					break;
				case ListChangedType.ItemChanged:
					RecalculateBurndownDependents();
					break;
				default:
					throw new NotImplementedException();
			}
		}

		protected override void Dispose(bool disposing)
		{
			base.Dispose(disposing);

			if (disposing)
			{
				CurrentSprintBurndownDataPoints = null;
			}
		}

		#endregion
	}
}